package fr.inserm.umr915.vcfannotator;


import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import fr.inserm.umr915.libjson.Node;
import fr.inserm.umr915.libjson.NodeFactory;
import fr.inserm.umr915.libjson.ObjectNode;
import fr.inserm.umr915.libjson.bdb.NodeBinding;
import fr.inserm.umr915.libjson.parser.JSONParser;
import fr.inserm.umr915.libjson.parser.ParseException;
import fr.inserm.umr915.vcfannotator.annotators.ChromosomeNameConverter;
import fr.inserm.umr915.vcfannotator.annotators.DefaultChromosomeNameConverter;
import fr.inserm.umr915.vcfannotator.annotators.PolyphenAnnotator;
import fr.inserm.umr915.vcfannotator.annotators.SiftAnnotator;
import fr.inserm.umr915.vcfannotator.io.IOUtils;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.io.InputStreamResource;

import com.sleepycat.je.Cursor;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;

/**
 * 
 * VCFAnnotator
 *
 */
public class VCFAnnotator
	{
	private final static Log LOG=LogFactory.getLog("vcfannotator");
	private BaseDatabase baseDatabase=null;
	private NodeFactory nodeFactory=null;
	private NodeBinding nodeBinding=null;
	private Variant.Binding variantBinding=new Variant.Binding();
	//private String buildVersion="hg19";
	private ChromosomeNameConverter chromosomeNameConverter=new DefaultChromosomeNameConverter();
	
	private static class Param
		{
		
		}
	
	private static class AnnotateAVcfFileParam
		extends Param
		{
		String build=null;
		String jsonInputUri;
		String vcfInUri;
		File vcfOut;
		}
	
	/**
	 * VCFAnnotator
	 */
	private VCFAnnotator()
		{
		this.baseDatabase=new BaseDatabase();
		this.nodeFactory=new NodeFactory();
		this.nodeBinding=new NodeBinding(this.nodeFactory);
		}

	
	public synchronized void close()
		{
		LOG.info("closing");
		
		if(this.baseDatabase!=null)
			{
			this.baseDatabase.close();
			}
		this.baseDatabase=null;
		}
	
	
	
	
	
	private void loadJsonFile(String jsonInUri) throws IOException
		{
		if(jsonInUri==null) return;
		InputStream in=null;
		DatabaseEntry key=new DatabaseEntry();
		DatabaseEntry data=new DatabaseEntry();
		try
			{
			in=IOUtils.openStream(jsonInUri);
			JSONParser parser=new JSONParser(in);
			parser.openArray();
			for(;;)
				{
				ObjectNode object=parser.objectNode();
				
				ObjectNode keyNode=object.get("key").asObjectNode();
				ObjectNode valueNode=object.get("value").asObjectNode();
				nodeBinding.objectToEntry(keyNode, key);
				nodeBinding.objectToEntry(valueNode,data);
				if(this.baseDatabase.getJsonDatabase().put(null, key, data)!=OperationStatus.SUCCESS)
					{
					throw new IOException("can insert:"+object);
					}
				if(parser.commaOrCloseArray()==']') break;
				}
			parser.eof();
			}
		catch(ParseException err)
			{
			throw new IOException(err);
			}
		finally
			{
			if(in!=null) in.close();
			}
		}
	
	private void dump(File file)  throws IOException
		{
		PrintWriter out=null;
		if(file==null)
			{
			out=new PrintWriter(System.out);
			}
		else if(file.getName().toLowerCase().endsWith(".gz"))
			{
			out=new PrintWriter(new GZIPOutputStream(new FileOutputStream(file)));
			}
		else
			{
			out=new PrintWriter(file);
			}
		dump(out);
		out.flush();
		if(file!=null) out.close();
		}
	
	private void dump(PrintWriter out) throws IOException
		{
		LOG.info("dump "+this.baseDatabase.getJsonDatabase().count()+" "+this.baseDatabase.getVariantDatabase().count());	
		Cursor c=null;
		try
			{
			LOG.info("dumping");
			DatabaseEntry key=new DatabaseEntry();
			DatabaseEntry value=new DatabaseEntry();
			boolean first=true;
			out.print("[\n");
			LOG.info("open cursor");
			c=this.baseDatabase.getJsonDatabase().openCursor(null, null);
			while(c.getNext(key, value, LockMode.DEFAULT)==OperationStatus.SUCCESS)
				{
				
				if(!first) out.print(",\n");
				first=false;
				out.print("{\"key\":");
				this.nodeBinding.entryToObject(key).print(out);
				out.print(",\"value\":");
				this.nodeBinding.entryToObject(value).print(out);
				out.print("}");
				}
			LOG.info("end cursor");
			out.print("\n]\n");
			out.flush();
			}
		catch (Exception e)
			{
			LOG.info("errooor!!!!");
			throw new RuntimeException(e);
			}
		finally
			{
			if(c!=null) c.close();
			}
		LOG.info("end dump");
		}
	
	private ObjectNode variantToNode(Variant var)
		{
		ObjectNode o=this.nodeFactory.createObjectNode();
		o.put("_class","variation");
		o.put("build",var.getBuild());
		o.put("chrom",var.getChrom());
		o.put("pos1",var.getPosition());
		o.put("ref",var.getRef());
		o.put("alt",var.getAlt());
		return o;
		}
	
	
	
	private void loadVcf(String buildVersion,InputStream input) throws IOException
		{
		LOG.info("reading VCF");
		DatabaseEntry key=new DatabaseEntry();
		DatabaseEntry value=new DatabaseEntry();
		BufferedReader in=new BufferedReader(new InputStreamReader(input));
		String line;
		while((line=in.readLine())!=null)
			{
			if(line.startsWith("#")) continue;
			
			String tokens[]=line.split("[\t]");
			String chrom=chromosomeNameConverter.convertChromosome(tokens[0]);
			
			Variant var=new Variant(
				buildVersion,
				chrom,
				Integer.parseInt(tokens[1]),
				tokens[3].toUpperCase(),
				tokens[4].toUpperCase()
				);
			
			this.variantBinding.objectToEntry(var, key);
			if(this.baseDatabase.getVariantDatabase().get(null, key, value, LockMode.DEFAULT)==OperationStatus.SUCCESS)
				{
				//LOG.info("already in database "+var);
				//already exists
				continue;
				}
			
			this.nodeBinding.objectToEntry(variantToNode(var),key);
			this.nodeBinding.objectToEntry(this.nodeFactory.createObjectNode(),value);
			if(this.baseDatabase.getJsonDatabase().put(null, key, value)!=OperationStatus.SUCCESS)
				{
				throw new IOException("cannot insert:"+line);
				}
				
			}
		LOG.info("done reading VCF");
		}
	
	
	
	
	
	private void annotateAVcf(AnnotateAVcfFileParam param)
		throws AnnotationException,IOException
		{
		final String KEY="JSON";
		final Pattern TAB=Pattern.compile("\t");
		DatabaseEntry key=new DatabaseEntry();
		DatabaseEntry value=new DatabaseEntry();
		BufferedReader vcfReader=null;
		PrintWriter out=null;
		try {
			if(param.vcfOut!=null)
				{
				LOG.info("writing VCF to "+param.vcfOut);
				out=new PrintWriter(param.vcfOut);
				}
			else
				{
				out=new PrintWriter(System.out);
				}
			
			this.baseDatabase.open();
			this.loadJsonFile(param.jsonInputUri);
			
			if(param.vcfInUri==null)
				{
				LOG.info("reading from stdin");
				vcfReader=new BufferedReader(new InputStreamReader(System.in));
				}
			else
				{
				LOG.info("reading from uri:"+param.vcfInUri);
				vcfReader=IOUtils.openReader(param.vcfInUri);
				}
			this.loadJsonFile(param.vcfInUri);
			int infoColumn=-1;
			String line;
			String tokens[];
			List<String> headers=new ArrayList<String>();
			while((line=vcfReader.readLine())!=null)
				{
				if(line.startsWith("#"))
					{
					if(infoColumn!=-1) throw new IOException(line);
					headers.add(line);
					if(line.startsWith("#CHROM\t"))
						{
						tokens=TAB.split(line);
						for(int i=0;i< tokens.length;++i)
							{
							if(tokens[i].equals("INFO"))
								{
								infoColumn=i;
								break;
								}
							}
						if(infoColumn==-1) throw new IOException("Cannot find INFO in "+line);
						for(int i=0;i< headers.size()-1;++i)
							{
							out.println(headers.get(i));
							}
						out.println("#<ID="+KEY+",Number=1,Type=String,Description=\"JSON annotation\">");
						out.println(line);
						}
					}
				else
					{
					if(infoColumn==-1) throw new IOException("Column INFO not defined");
					tokens=TAB.split(line);
					if(tokens.length<=infoColumn) throw new IOException("Illegal number of columns");
					
					Variant var=new Variant(
						param.build,
						this.chromosomeNameConverter.convertChromosome(tokens[0]),
						Integer.parseInt(tokens[1]),
						tokens[3].toUpperCase(),
						tokens[4].toUpperCase()
						);
					
					this.variantBinding.objectToEntry(var, key);
					if(this.baseDatabase.getVariantDatabase().get(null, key, value, LockMode.DEFAULT)==OperationStatus.SUCCESS)
						{
						out.println(line);
						continue;
						}
					Node node=this.nodeBinding.entryToObject(value);
					String info=tokens[infoColumn];
					if(info.equals(".")) info="";
					if(info.length()>0) info+=";";
					info+=KEY;
					info="=";
					info+=node.getJson();
					tokens[infoColumn]=info;
					for(int i=0;i< tokens.length;++i)
						{
						if(i>0) out.print('\t');
						out.print(tokens[i]);
						}
					out.println();
					}
				}
			}
		catch (Exception error)
			{
			throw new AnnotationException(error);
			}
		finally
			{
			if(out!=null)  { out.flush(); out.close();}
			if(vcfReader!=null) vcfReader.close();
			}
		}
	
	
	
	public static void main(String[] args)
		{
		String jsonInUri=null;
		String build=null;
		final VCFAnnotator app=new VCFAnnotator();
		try
			{
			String springBeansUri=null;
			String springBeanId=null;
			String httpProxyHost=null;
			String httpProxyPort=null;
			String ftpProxyHost=null;
			String ftpProxyPort=null;
			File fileOut=null;
			String program=null;
			int optind=0;
			while(optind< args.length)
				{
				if(args[optind].equals("-h") ||
				   args[optind].equals("-help") ||
				   args[optind].equals("--help"))
					{
					System.err.println("Options:");
					System.err.println(" -h help; This screen.");
					System.err.println(" -p <program> ");
					System.err.println(" -b <build> e.g: hg19");
					System.err.println(" -i <json-in>");
					System.err.println(" -o <file-out>");
					System.err.println(" -b <build>");
					System.err.println(" -f <uri:/to/spring/beans.xml>");
					System.err.println(" -n <beanid>");
					System.err.println(" -httpProxyHost|httpProxyPort|ftpProxyHost|ftpProxyPort <value>");
					System.err.println();
					System.err.println("-p loadvcf -b build [-i file.json] [-o out] (stdin|vcf)");
					System.err.println("-p annotatevcf -b build -i file.json [-o out] (stdin|vcf)");
					System.err.println("-p annotatejson -i file.json [-o out] -f uri:/to/spring/beans.xml -n beanid");
					System.err.println("-p loadpolyphen -i file.json -b <build> [-o file] (stdin|polyphen)");
					System.err.println("-p loadsift -i file.json -b <build> [-o file] (stdin|polyphen)");
					
					return;
					}
				else if(args[optind].equals("-httpProxyHost"))
					{
					httpProxyHost=args[++optind];
					}
				else if(args[optind].equals("-httpProxyPort"))
					{
					httpProxyPort=args[++optind];
					}
				else if(args[optind].equals("-ftpProxyHost"))
					{
					ftpProxyHost=args[++optind];
					}
				else if(args[optind].equals("-ftpProxyPort"))
					{
					ftpProxyPort=args[++optind];
					}
				else if(args[optind].equals("-p"))
					{
					program=args[++optind];
					}
				else if(args[optind].equals("-i"))
					{
					jsonInUri=args[++optind];
					}
				else if(args[optind].equals("-o"))
					{
					fileOut=new File(args[++optind]);
					}
				else if(args[optind].equals("-build") || args[optind].equals("-b"))
					{
					build= args[++optind];
					}
				else if(args[optind].equals("-f"))
					{
					springBeansUri=args[++optind];
					}
				else if(args[optind].equals("-n"))
					{
					springBeanId=args[++optind];
					}
				else if(args[optind].equals("--"))
					{
					optind++;
					break;
					}
				else if(args[optind].startsWith("-"))
					{
					System.err.println("Unknown option "+args[optind]);
					return;
					}
				else 
					{
					break;
					}
				++optind;
				}
			if(httpProxyHost!=null)
				{
				System.setProperty("http.proxyHost",httpProxyHost);
				}
			if(httpProxyPort!=null)
				{
				System.setProperty("http.proxyPort",httpProxyPort);
				}
			if(ftpProxyHost!=null)
				{
				System.setProperty("ftp.proxyHost",ftpProxyHost);
				}
			if(ftpProxyPort!=null)
				{
				System.setProperty("ftp.proxyPort",ftpProxyPort);
				}
			
			LOG.info("program is "+program);
			/********************************************************************/
			if(program==null)
				{
				System.err.println("Undefined program.");
				return;
				}
			/********************************************************************/
			else if(program.equals("loadvcf"))
				{
				if(build==null)
					{
					System.err.println("Undefined build");
					return;
					}
				
				app.baseDatabase.open();
				if(jsonInUri!=null)
					{
					app.loadJsonFile(jsonInUri);
					}
				
				if(optind==args.length)
					{
					app.loadVcf(build,System.in);
					}
				else
					{
					while(optind< args.length)
						{
						String filename=args[optind++];
						LOG.info(filename);
						InputStream in=IOUtils.openStream(filename);
						app.loadVcf(build,in);
						in.close();
						}
					}
				
				app.dump(fileOut);
				}
			/********************************************************************/
			else if(program.equals("annotatevcf"))
				{
				
				AnnotateAVcfFileParam param=new AnnotateAVcfFileParam();
				if(jsonInUri==null)
					{
					System.err.println("Undefined jsonUri");
					return;
					}
				param.jsonInputUri= jsonInUri;
				param.vcfOut= fileOut;
				if(build==null)
					{
					System.err.println("Undefined build");
					return;
					}
				param.build=build;
				
				if(optind+1==args.length)
					{
					param.vcfInUri=args[optind+1];
					}
				else if(args.length!=optind)
					{
					System.err.println("Illegal number of arguments.");
					return;
					}
				app.annotateAVcf(param);
				}
			/********************************************************************/
			else if(program.equals("annotatejson"))
				{
				
				
				if(springBeansUri==null)
					{
					System.err.println("Spring bean uri missing");
					return;
					}
				
				if(springBeanId==null)
					{
					System.err.println("Spring bean id missing");
					return;
					}
				
				if(args.length!=optind)
					{
					System.err.println("Illegal number of arguments");
					return;
					}
				
				FileSystemXmlApplicationContext context = null;
				context =new FileSystemXmlApplicationContext(springBeansUri);
				AnnotatorList  annotatorList= (AnnotatorList)context.getBean(springBeanId);
		        if(annotatorList==null) throw new AnnotationException("cannot get bean "+springBeanId);
		        LOG.info(annotatorList);
		        
		        if(jsonInUri==null)
					{
		        	System.err.println("Undefined json source");
					return;
					}
		        
		        app.baseDatabase.open();
				app.loadJsonFile(jsonInUri);
				annotatorList.annotate(app.baseDatabase);
				
				app.dump(fileOut);
				}
			else if(program.equals("loadpolyphen"))
				{
				
				AnnotatorList list=new AnnotatorList();
				if(build==null)
					{
					System.err.println("Undefined build");
					return;
					}
				
				
				if(jsonInUri==null)
					{
					System.err.println("JSON-in missing");
					return;
					}
				
				app.baseDatabase.open();
				app.loadJsonFile(jsonInUri);
				
				if(optind==args.length)
					{
					PolyphenAnnotator polyphenAnnotator=new PolyphenAnnotator();
					polyphenAnnotator.setBuild(build);
					polyphenAnnotator.setInputStreamSource(new InputStreamResource(System.in));
					list.getAnnotators().add(polyphenAnnotator);
					}
				else
					{
					while(optind< args.length)
						{
						String filename=args[optind++];
						LOG.info(filename);
						PolyphenAnnotator polyphenAnnotator=new PolyphenAnnotator();
						polyphenAnnotator.setBuild(build);
						polyphenAnnotator.setInputStreamSource(IOUtils.createFileInputSource(filename));
						list.getAnnotators().add(polyphenAnnotator);
						}
					}
				list.annotate(app.baseDatabase);
				app.dump(fileOut);
				}
			else if(program.equals("loadsift"))
				{
				AnnotatorList list=new AnnotatorList();
				if(build==null)
					{
					System.err.println("Undefined build");
					return;
					}
				
				if(jsonInUri==null)
					{
					System.err.println("JSON-in missing");
					return;
					}
				
				app.baseDatabase.open();
				app.loadJsonFile(jsonInUri);
				
				if(optind==args.length)
					{
					SiftAnnotator siftAnnotator=new SiftAnnotator();
					siftAnnotator.setBuild(build);
					siftAnnotator.setInputStreamSource(new InputStreamResource(System.in));
					list.getAnnotators().add(siftAnnotator);
					}
				else
					{
					while(optind< args.length)
						{
						String filename=args[optind++];
						LOG.info(filename);
						SiftAnnotator siftAnnotator=new SiftAnnotator();
						siftAnnotator.setBuild(build);
						siftAnnotator.setInputStreamSource(IOUtils.createFileInputSource(filename));
						list.getAnnotators().add(siftAnnotator);
						}
					}
				list.annotate(app.baseDatabase);
				app.dump(fileOut);
				}
			else
				{
				System.err.println("Unknown Program :"+program);
				}
			} 
		catch(Throwable err)
			{
			err.printStackTrace();
			}
		finally
			{
			app.close();
			}
		}
	}
